# init

在将路由实例挂载到 Vue 实例后,会执行 init 函数。

核心源码如下:

VueRouter.prototype.init = function init (app /* Vue component instance */) {
  // ...
  var history = this.history; 
  if (history instanceof HTML5History || history instanceof HashHistory) {
    var handleInitialScroll = function (routeOrError) {
      var from = history.current;
      var expectScroll = this$1.options.scrollBehavior;
      var supportsScroll = supportsPushState && expectScroll;

      if (supportsScroll && 'fullPath' in routeOrError) {
        handleScroll(this$1, routeOrError, from, false);
      }
    };
    var setupListeners = function (routeOrError) {
      history.setupListeners();
      handleInitialScroll(routeOrError);
    };
    // 调用 transitionTo 进行路由的跳转
    history.transitionTo(
      history.getCurrentLocation(),
      setupListeners,
      setupListeners
    );
  }

  // 将 app._route 设置为当前路由
  history.listen(function (route) {
    this$1.apps.forEach(function (app) {
      app._route = route;
    });
  });
  // ...
}

该函数的核心逻辑是:

  • 跳转到目标路由,主要调用history.transitionTo函数。
  • 改变路由的 URL 之后,将 app._route 设置为当前路由,由于在初始化的时候已经通过将 _route 设置为响应式的,所以一旦 _route 改变,对应的视图也会进行变更。

# transitionTo

该函数的核心源码如下:

History.prototype.transitionTo = function transitionTo (
  location,
  onComplete,
  onAbort
) {
  var this$1 = this;

  var route;
  // catch redirect option https://github.com/vuejs/vue-router/issues/3201
  try {
    // 通过当前 location 匹配到当前路由信息
    route = this.router.match(location, this.current);
  } catch (e) {
    // 错误处理
    this.errorCbs.forEach(function (cb) {
      cb(e);
    });
    throw e
  }
  var prev = this.current;
  this.confirmTransition(
    route,
    function () {
      this$1.updateRoute(route);
      onComplete && onComplete(route);
      this$1.ensureURL();
      this$1.router.afterHooks.forEach(function (hook) {
        hook && hook(route, prev);
      });

      // fire ready cbs once
      if (!this$1.ready) {
        this$1.ready = true;
        this$1.readyCbs.forEach(function (cb) {
          cb(route);
        });
      }
    },
    function (err) {
      if (onAbort) {
        onAbort(err);
      }
      if (err && !this$1.ready) {
        // 错误处理
        if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
          this$1.ready = true;
          this$1.readyErrorCbs.forEach(function (cb) {
            cb(err);
          });
        }
      }
    }
  );
};

该函数接收函数参数:

  • 当前的 location:通过当前 location 匹配到当前路由信息;
  • onComplete:执行成功回调;
  • onAbort:执行失败回调。

接着,该函数内部的主要核心是调用confirmTransition

# confirmTransition

该函数主要源码如下:

History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {
  var this$1 = this;

  var current = this.current;
  this.pending = route; // 保存当前路由
  var abort = function (err) {
    // 错误处理函数
  };
  
  var lastRouteIndex = route.matched.length - 1; // 上个路由在路由配置表中的索引
  var lastCurrentIndex = current.matched.length - 1; // 当前路由在路由配置表中的索引
  
  // 判断是否是同一个路由
  if (
    isSameRoute(route, current) &&
    // in the case the route map has been dynamically appended to
    lastRouteIndex === lastCurrentIndex &&
    route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
  ) {
    this.ensureURL();
    if (route.hash) {
      handleScroll(this.router, current, route, false);
    }
    return abort(createNavigationDuplicatedError(current, route))
  }

  // 通过对比路由解析出可复用的组件,需要渲染的组件,失活的组件
  var ref = resolveQueue(
    this.current.matched,
    route.matched
  );
    var updated = ref.updated; // 可复用的组件
    var deactivated = ref.deactivated; // 失活的组件
    var activated = ref.activated; // 需要重新渲染的组件
  // 路由守卫数组
  var queue = [].concat(
    // 组件内离开路由守卫
    extractLeaveGuards(deactivated),
    // 全局离开钩子
    this.router.beforeHooks,
    // 组件内更新钩子
    extractUpdateHooks(updated),
    // 需要渲染组件 enter 守卫钩子
    activated.map(function (m) { return m.beforeEnter; }),
    // 处理异步组件
    resolveAsyncComponents(activated)
  );

  // runQueue 第二个参数,主要用于接收路由守卫并执行对应的钩子函数
  var iterator = function (hook, next) {
    if (this$1.pending !== route) {
      return abort(createNavigationCancelledError(current, route))
    }
    try {
      hook(route, current, 
        function (to) {
          if (to === false) {
            // next(false) -> abort navigation, ensure current URL
            this$1.ensureURL(true);
            abort(createNavigationAbortedError(current, route));
          } else if (isError(to)) {
            this$1.ensureURL(true);
            abort(to);
          } else if (
            typeof to === 'string' ||
            (typeof to === 'object' &&
              (typeof to.path === 'string' || typeof to.name === 'string'))
          ) {
            // next('/') or next({ path: '/' }) -> redirect
            abort(createNavigationRedirectedError(current, route));
            if (typeof to === 'object' && to.replace) {
              this$1.replace(to);
            } else {
              this$1.push(to);
            }
          } else {
            // 执行下一次迭代
            next(to);
          }
        });
    } catch (e) {
      abort(e);
    }
  };

  // 通过 runQueue 是实现异步函数同步化执行逻辑
  runQueue(queue, iterator, function () {
    // 执行 enter 路由守卫钩子
    var enterGuards = extractEnterGuards(activated);
    var queue = enterGuards.concat(this$1.router.resolveHooks);
    runQueue(queue, iterator, function () {
      if (this$1.pending !== route) {
        return abort(createNavigationCancelledError(current, route))
      }
      this$1.pending = null;
      onComplete(route); // 执行回调
      if (this$1.router.app) {
        this$1.router.app.$nextTick(function () {
          handleRouteEntered(route);
        });
      }
    });
  });
};

该函数的主要逻辑:执行一系列路由守卫函数。

runQueue主要实现异步函数同步化执行:

function runQueue (queue, fn, cb) {
  var step = function (index) {
    if (index >= queue.length) {
      // 迭代结束,执行回调
      cb();
    } else {
      if (queue[index]) {
        fn(queue[index], function () {
          step(index + 1); // 执行下一个迭代
        });
      } else {
        step(index + 1); // 执行下一个迭代
      }
    }
  };
  step(0); // 首次迭代
}

# 路由守卫调用执行逻辑

以组件内离开路由守卫extractLeaveGuards(deactivated)为例。

首先,进入 extractLeaveGuards 函数,函数参数 deactivated 为通过 resolveQueue 解析出的失活路由列表。

function extractLeaveGuards (deactivated) {
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

函数内调用 extractGuards 函数,函数参数为:

  • records:路由记录列表。这里是通过 resolveQueue 解析出的失活路由列表 deactivated。records 的列表项结构如下:
interface RouteRecord {
  path: string
  regex: RegExp
  components: Dictionary<Component>
  instances: Dictionary<Vue>
  name?: string
  parent?: RouteRecord
  redirect?: RedirectOption
  matchAs?: string
  meta: RouteMeta
  beforeEnter?: (
    route: Route,
    redirect: (location: RawLocation) => void,
    next: () => void
  ) => any
  props:
    | boolean
    | Object
    | RoutePropsFunction
    | Dictionary<boolean | Object | RoutePropsFunction>
}
  • name:路由守卫名,这里是 beforeRouteLeave。
  • bind:将路由守卫的 this 绑定到具体组件实例上的函数,并执行路由守卫。下面是 bindGuard 函数源码:
function bindGuard (guard, instance) {
  if (instance) {
    return function boundRouteGuard () {
      return guard.apply(instance, arguments)
    }
  }
}
  • reverse:因为某些钩子函数需要从子执行到父,所以需要判断是否需要反转。

接下来进入 extractGuards 函数:

function extractGuards (
  records,
  name,
  bind,
  reverse
) {
  var guards = flatMapComponents(records, function (def, instance, match, key) {
    var guard = extractGuard(def, name);
    if (guard) {
      return Array.isArray(guard)
        ? guard.map(function (guard) { return bind(guard, instance, match, key); })
        : bind(guard, instance, match, key)
    }
  });
  return flatten(reverse ? guards.reverse() : guards)
}

extractGuards 函数调用 flatMapComponents 函数,传入路由列表和回调函数,最后返回路由守卫列表 guards。

进入 flatMapComponents 函数:

function flatMapComponents (
  matched,
  fn
) {
  return flatten(matched.map(function (m) { // 遍历路由列表
    return Object.keys(m.components).map(function (key) { return fn( // 遍历路由列表项中的每个路由组件
      m.components[key],
      m.instances[key],
      m, key
    ); })
  }))
}

// 二级数组扁平化
function flatten (arr) {
  return Array.prototype.concat.apply([], arr)
}

flatMapComponents 函数执行逻辑:遍历路由列表,然后获取每个路由列表项对应的路由组件列表,接着遍历路由组件列表,获取每个路由组件的组件定义和组件实例,最后执行传入的回调函数,该回调函数也就是在 extractGuards 函数中调用 flatMapComponents 传入的第二个参数。回调函数源码如下:

function (def, instance, match, key) {
  var guard = extractGuard(def, name);
  if (guard) {
    return Array.isArray(guard)
      ? guard.map(function (guard) { return bind(guard, instance, match, key); })
    : bind(guard, instance, match, key)
  }
}

回调函数内部执行逻辑:提取目标路由守卫函数,将路由守卫的 this 绑定到具体组件实例上的函数,并执行路由守卫。